######################################## Go 概要 ######################################## Go 代码结构 **************************************** 与 Rust 不同的是,Go 将代码划分为包。一个 Go 语言必须在每个源文件的头部显式写明所属包名。另一点是 Go 对代码的格式进行了严格要求,例如大括号不能换行等 Go 语言与 Js 类似,编译器会在语句末尾自动添加分号。但是如果希望在同一行写多个语句,那么需要手动添加分号 Go 语言的注释与 C++ 相同,分为行注释和块注释 基础语法 **************************************** Go 语言提供了布尔、浮点数、复数、字符串、指针、数组、结构等类型。并且使用了后置类型声明。 另外还提供了一个 nil 值/类型 以下几种类型为 nil: .. code-block:: go var a *int var a []int var a map[string] int var a chan int var a func(string) int var a error // error 是接口 Go 语言 **声明** 变量有以下几种方式: .. code-block:: go var a = 10 a := 10 var a, b, c = 1, 2, 3 var a, b, c int32 = 1, 2, 3 // 下面的写法一般用于声明全局变量 var ( A int64 B string ) 局部变量声明后必须要使用,否则产生编译错误。全局变量则不然 声明常量使用 const 修饰 数组的声明方式为: .. code-block:: go a := []int{1, 2, 3, 4, 5} for _, it := range a { fmt.Println(it) } 数组的大小可以手动指定,也可以直接忽略 Go 语言中的空指针被称为 nil,统一用来代表空值 另外介绍一个关键字 **iota** iota 用于 const 表达式中,让编译期自动计算它的值,计算方式如下: - 从第一个 const 表达式开始计算,初值为 0 - 每次碰到一个 const 变量加一 - 没有显式初始化的变量将被编译期赋值 例如: .. code-block:: go const ( a = 2 b = iota // b = 1 c // c = 3 d = "yes" e // e = "yes" k = iota // 5 j // j = 6 ) 从中也可以看出这种初始化的特点:未显式初始化的变量会延续上一个变量的类型和值。多次使用 itoa 不会更改编译期的计数 Go 语言中支持的运算符与 C++ 完全相同 引用类型 **************************************** Go 语言中单字数据使用拷贝语义,多字数据使用引用语义 条件语句 **************************************** 条件语句与 Rust 一样: .. code-block:: go a := true if a { fmt.Println("true") } 循环语句 **************************************** Go 语言中的循环语句如下: .. code-block:: go // 一般格式 for i := 0; i < 10; i++ { fmt.Println(i) } i := 1 // 类 while 形式 for i < 10 { fmt.Println(i) i++ } // for true{} for { break } str := "hello" // 展开 dict, list for index, ch := range str { fmt.Printf("%d, %c\n",index , ch) } 函数 **************************************** Go 语言中的函数也采用了后置类型声明,但是和 Rust 不同的是不再需要箭头: .. code-block:: go func add(a, b int) int { return a + b } func main() { fmt.Println(add(1, 2)) } 还可以返回多个值 .. code-block:: go func swap(x, y int) (int, int) { return y, x } Go 语言中的参数默认是值传递,如果需要引用传递,则需要使用指针: .. code-block:: go func plus(a *int) int { *a++ return *a } func main() { a := 10 fmt.Println(plus(&a)) } 结构体 **************************************** .. code-block:: go type Student struct { id string name string age uint16 } func main() { stu1 := Student{id: "12345", name: "小明", age: 16} fmt.Println(stu1.name) stuPtr := &stu1 fmt.Println(stuPtr.age) } 从中也可以看出来 Go 语言中的指针语法声明的实际上是类似于引用的变量 切片 **************************************** Go 语言切片的方式和其它语言相同: .. code-block:: go arr := []int{1, 2, 3, 4, 5} slice1 := arr[1:] slice2 := arr[:2] slice3 := arr[1:2] fmt.Println("slice1") for _, it := range slice1 { fmt.Println(it) } fmt.Println("slice2") for _, it := range slice2 { fmt.Println(it) } fmt.Println("slice3") for _, it := range slice3 { fmt.Println(it) } defer **************************************** 关键字 defer 用来将一个 **函数** 推迟到函数结束前调用。并遵循以下规则: - 函数的调用顺序与 defer 声明的顺序相反。也就是说先声明的被后调用 - defer 执行的函数的参数在声明的时候被固定。也就是说参数的传递是拷贝而不是引用 .. code-block:: go func main(){ if true { defer fmt.Println("defer") } fmt.Println("main body") } 输出结果为 :: main body defer 字典 **************************************** 字典的使用方式为: .. code-block:: go map1 := map[string]string{} map1["a"] = "b" map1["c"] = "d" for k, v := range map1 { fmt.Println(k, v) } 字典底层使用的是 hash 表,因此是无序的 面向对象 **************************************** Go 语言和 Rust 类似,将数据和方法进行分离,函数的实现是面向接口的。 .. code-block:: go type Student struct { id string name string age int16 } type show interface { output() } func (stu Student) output() { fmt.Println(stu.id, stu.name, stu.age) } func main() { stu := Student{"12345", "小明", 16} stu.output() } 可以看到,实际上和 Rust 走的路子相同。可以理解为特化接口。只是 Rust 从代码中可以理解出来,而 Go 相对隐晦 另外,和结构体绑定的函数称为方法,方法会将结构体按指针或值传递并绑定为参数: .. code-block:: go type Stu struct { id int } func (t Stu)setId(){ t.id = 20 } func (t *Stu)setPid(){ t.id = 30 } 如代码所示,setId 传入的结构体为按值传入,setPid 传入的结构体则是按指针传入。第一个方法自然没办法实现想要的语义。 如果使用值类型调用 setPid,那么传入的实际上是值的一个副本(毕竟右值是没办法取地址的) 匿名字段 **************************************** 如代码所示: .. code-block:: go type User struct { id int name string } type Manager struct { User } 这样,Manager 就获得了 User 的字段。这就是 Go 中的继承。自然,Manager 的同名方法也会覆盖 User 的同名方法 接口 **************************************** 同 C++ 中含纯虚函数的类一样,Go 中的接口 **是类型的一种** 。接口用来保证实现此接口的结构体必定实现了此接口的函数。 接口同样可以被继承。另外,与其他语言中显式实现接口不同,Go 中 **实现了接口中的所有方法也就实现了接口** 。例如: .. code-block:: go type Anmial interface { say() } type Dog struct { name string } func (self Dog) say() { fmt.Println(self.name) } func main() { var xab Anmial = Dog{} dog := Dog{} dog.name = "Dog" xab.say() } 如代码所示,只要实现了接口需要的函数,那么就实现了接口 错误处理 **************************************** 和 Rust 相同,错误被声明为一个接口,然后通过返回值返回出去 .. code-block:: go func Sqrt(f float64) (float64, error) { if f < 0 { return 0, errors.New("error") } else { return math.Sqrt(f), errors.New("") } } func main() { if _, err := Sqrt(-1); err.Error() == "error" { fmt.Println("error") } } 定义一个接口的方式只是简单地实现 Error 接口: .. code-block:: go type ParameterError struct { Message string } func (prarm *ParameterError) Error() string { return prarm.Message } 类型断言 **************************************** 类型断言可以当作强制类型转换,用来判断值是否能转换到目的类型。其格式为: .. code-block:: go value, ok := x.(T) 当 x 可以转为类型 T 时,ok 不为 nil,且由 value 储存结果值。一个常见的用法是: .. code-block:: go if exitError, ok := err.(*exec.ExitError); ok { os.Exit(exitError.ExitCode()) } else { panic(err) } 协程 **************************************** 协程是用户级线程,除此之外和线程没什么区别(从外观上)。Go 中协程的开启非常简单: .. code-block:: go func output(str string) { for i := 10; i > 0; i-- { time.Sleep(100 * time.Millisecond) fmt.Println(str) } } func main() { go output("hello") output("world") } 注意:如果主线程结束的太快,协程可能由于得不到调度而退出(外观和线程一样),为了解决这个问题,可以使用 WaitGroup: .. code-block:: go import ( "sync" ) var waitGroup sync.WaitGroup func func1(){ defer waitGroup.Done() } func main(){ waitGroup.Add(1) go func1() waitGroup.Wait() } WaitGroup.Add 设置了需要等待的协程数量,而 WaitGroup.Done() 会将该值减小 1。当减小为零时,WaitGroup.Wait() 退出 通道 **************************************** 通道用来在两个协程之间传递数据,通道有两种定义方式: .. code-block:: go ch1 := make(chan int) // 声名一个无缓存的通道 ch2 := make(chan int, 3) // 声名一个具有三个容量缓存的通道 - 如果尝试在缓存已满的情况下进行写,写端就会被阻塞 - 如果尝试读没有数据的通道,读端就会被阻塞 .. important:: 如果一个通道在运行时没有了读端,那么此通道会被自动 close,再进行写就会触发 panic 此外,通道还可以被关闭或者置为 nil : .. code-block:: go ch1 := make(chan bool) close(ch1) ch2 := make(chan bool) ch2 = nil - 对已经关闭的通道进行写会触发 panic - 对已经关闭的通道进行读会返回相关类型的零值 - 对值为 nil 的通道进行读写会触发 panic - 如果通道被置为 nil,则不会被 select 选中 一个无缓存的使用例子为: .. code-block:: go func sum(arr []int, c chan int) { sum := 0 for _, v := range arr { sum += v } c <- sum } func main() { arr := []int{1, 2, 3, 4, 5} c := make(chan int) go sum(arr[:3], c) go sum(arr[3:], c) x, y := <-c, <-c fmt.Println(x, y) } 调试 **************************************** Go 程序本身可以被 gdb 调试,但是默认的构建不利于调试。要想启用完整的 gdb 支持,构建时需要使用 :: go build -gcflags "-N -l" 主要是禁用了优化 在发布时构建则为 :: go build -ldflags “-s -w” 导出变量和函数 **************************************** 与 Python 相似,Go 使用约定来保证变量和函数的可见性。 只有首字母为大写的函数和变量才能为外部所看到。 例如: .. code-block:: go type Student struct{ ID int // 外部可见 name string // 外部不可见 } stu := Student{ ID: 10, // 正确 name : "xiaoming" // 错误 } 如果尝试初始化 name 变量,则会报错 :: implicit assignment to unexported field message in xxx literal compiler (UnexportedLitField) 与结构体类似,包中的变量和函数也是以类似的方式导出的 调用私有方法和成员 **************************************** 调用私有方法的方式是使用 :command:`go:linkname` 注释: .. code-block:: go //go:linkname setResources github.com/containerd/cgroups/v2.setResources func setResources(path string, resources *v2.Resources) error 调用私有成员的方式是使用 :command:`unsafe` : .. code-block:: go type Manager struct { // v2.Manager for access private path _ string path string } p := *(*Manager)(unsafe.Pointer(manager)) if err := setResources(p.path, &v.Resources); err != nil { if err := manager.Delete(); err != nil { return err } return err } Go Mod **************************************** 使用指定版本的分支 :: github.com/containerd/cgroups main 然后直接 :command:`go build` ,会自动更改为 hash :: github.com/containerd/cgroups v1.0.4-0.20220317195426-f8328fdc061d